// ASN.1 JavaScript decoder
// Copyright (c) 2008-2014 Lapo Luchini <lapo@lapo.it>
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */

/*global oids */
import { Int10 } from './int10';
var ellipsis = '\u2026';
var reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/;
var reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/;

function stringCut(str, len) {
    if (str.length > len) {
        str = str.substring(0, len) + ellipsis;
    }

    return str;
}

var Stream =
    /** @class */
    (function () {
        function Stream(enc, pos) {
            this.hexDigits = '0123456789ABCDEF';

            if (enc instanceof Stream) {
                this.enc = enc.enc;
                this.pos = enc.pos;
            } else {
                // enc should be an array or a binary string
                this.enc = enc;
                this.pos = pos;
            }
        }

        Stream.prototype.get = function (pos) {
            if (pos === undefined) {
                pos = this.pos++;
            }

            if (pos >= this.enc.length) {
                throw new Error('Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length);
            }

            return 'string' === typeof this.enc ? this.enc.charCodeAt(pos) : this.enc[pos];
        };

        Stream.prototype.hexByte = function (b) {
            return this.hexDigits.charAt((b >> 4) & 15) + this.hexDigits.charAt(b & 15);
        };

        Stream.prototype.hexDump = function (start, end, raw) {
            var s = '';

            for (var i = start; i < end; ++i) {
                s += this.hexByte(this.get(i));

                if (raw !== true) {
                    switch (i & 15) {
                        case 7:
                            s += '  ';
                            break;

                        case 15:
                            s += '\n';
                            break;

                        default:
                            s += ' ';
                    }
                }
            }

            return s;
        };

        Stream.prototype.isASCII = function (start, end) {
            for (var i = start; i < end; ++i) {
                var c = this.get(i);

                if (c < 32 || c > 176) {
                    return false;
                }
            }

            return true;
        };

        Stream.prototype.parseStringISO = function (start, end) {
            var s = '';

            for (var i = start; i < end; ++i) {
                s += String.fromCharCode(this.get(i));
            }

            return s;
        };

        Stream.prototype.parseStringUTF = function (start, end) {
            var s = '';

            for (var i = start; i < end; ) {
                var c = this.get(i++);

                if (c < 128) {
                    s += String.fromCharCode(c);
                } else {
                    if (c > 191 && c < 224) {
                        s += String.fromCharCode(((c & 31) << 6) | (this.get(i++) & 63));
                    } else {
                        s += String.fromCharCode(((c & 15) << 12) | ((this.get(i++) & 63) << 6) | (this.get(i++) & 63));
                    }
                }
            }

            return s;
        };

        Stream.prototype.parseStringBMP = function (start, end) {
            var str = '';
            var hi;
            var lo;

            for (var i = start; i < end; ) {
                hi = this.get(i++);
                lo = this.get(i++);
                str += String.fromCharCode((hi << 8) | lo);
            }

            return str;
        };

        Stream.prototype.parseTime = function (start, end, shortYear) {
            var s = this.parseStringISO(start, end);
            var m = (shortYear ? reTimeS : reTimeL).exec(s);

            if (!m) {
                return 'Unrecognized time: ' + s;
            }

            if (shortYear) {
                // to avoid querying the timer, use the fixed range [1970, 2069]
                // it will conform with ITU X.400 [-10, +40] sliding window until 2030
                m[1] = +m[1];

                if (+m[1] < 70) {
                    m[1] += 2000;
                } else {
                    m[1] += 1900;
                }
            }

            s = m[1] + '-' + m[2] + '-' + m[3] + ' ' + m[4];

            if (m[5]) {
                s += ':' + m[5];

                if (m[6]) {
                    s += ':' + m[6];

                    if (m[7]) {
                        s += '.' + m[7];
                    }
                }
            }

            if (m[8]) {
                s += ' UTC';

                if (m[8] != 'Z') {
                    s += m[8];

                    if (m[9]) {
                        s += ':' + m[9];
                    }
                }
            }

            return s;
        };

        Stream.prototype.parseInteger = function (start, end) {
            var v = this.get(start);
            var neg = v > 127;
            var pad = neg ? 255 : 0;
            var len;
            var s = ''; // skip unuseful bits (not allowed in DER)

            while (v == pad && ++start < end) {
                v = this.get(start);
            }

            len = end - start;

            if (len === 0) {
                return neg ? -1 : 0;
            } // show bit length of huge integers

            if (len > 4) {
                s = v;
                len <<= 3;

                while (((+s ^ pad) & 128) == 0) {
                    s = +s << 1;
                    --len;
                }

                s = '(' + len + ' bit)\n';
            } // decode the integer

            if (neg) {
                v = v - 256;
            }

            var n = new Int10(v);

            for (var i = start + 1; i < end; ++i) {
                n.mulAdd(256, this.get(i));
            }

            return s + n.toString();
        };

        Stream.prototype.parseBitString = function (start, end, maxLength) {
            var unusedBit = this.get(start);
            var lenBit = ((end - start - 1) << 3) - unusedBit;
            var intro = '(' + lenBit + ' bit)\n';
            var s = '';

            for (var i = start + 1; i < end; ++i) {
                var b = this.get(i);
                var skip = i == end - 1 ? unusedBit : 0;

                for (var j = 7; j >= skip; --j) {
                    if ((b >> j) & 1) {
                        s += '1';
                    } else {
                        s += '0';
                    }
                }

                if (s.length > maxLength) {
                    return intro + stringCut(s, maxLength);
                }
            }

            return intro + s;
        };

        Stream.prototype.parseOctetString = function (start, end, maxLength) {
            if (this.isASCII(start, end)) {
                return stringCut(this.parseStringISO(start, end), maxLength);
            }

            var len = end - start;
            var s = '(' + len + ' byte)\n';
            maxLength /= 2; // we work in bytes

            if (len > maxLength) {
                end = start + maxLength;
            }

            for (var i = start; i < end; ++i) {
                s += this.hexByte(this.get(i));
            }

            if (len > maxLength) {
                s += ellipsis;
            }

            return s;
        };

        Stream.prototype.parseOID = function (start, end, maxLength) {
            var s = '';
            var n = new Int10();
            var bits = 0;

            for (var i = start; i < end; ++i) {
                var v = this.get(i);
                n.mulAdd(128, v & 127);
                bits += 7;

                if (!(v & 128)) {
                    // finished
                    if (s === '') {
                        n = n.simplify();

                        if (n instanceof Int10) {
                            n.sub(80);
                            s = '2.' + n.toString();
                        } else {
                            var m = n < 80 ? (n < 40 ? 0 : 1) : 2;
                            s = m + '.' + (n - m * 40);
                        }
                    } else {
                        s += '.' + n.toString();
                    }

                    if (s.length > maxLength) {
                        return stringCut(s, maxLength);
                    }

                    n = new Int10();
                    bits = 0;
                }
            }

            if (bits > 0) {
                s += '.incomplete';
            }

            return s;
        };

        return Stream;
    })();

export { Stream };

var ASN1 =
    /** @class */
    (function () {
        function ASN1(stream, header, length, tag, sub) {
            if (!(tag instanceof ASN1Tag)) {
                throw new Error('Invalid tag value.');
            }

            this.stream = stream;
            this.header = header;
            this.length = length;
            this.tag = tag;
            this.sub = sub;
        }

        ASN1.prototype.typeName = function () {
            switch (this.tag.tagClass) {
                case 0:
                    // universal
                    switch (this.tag.tagNumber) {
                        case 0:
                            return 'EOC';

                        case 1:
                            return 'BOOLEAN';

                        case 2:
                            return 'INTEGER';

                        case 3:
                            return 'BIT_STRING';

                        case 4:
                            return 'OCTET_STRING';

                        case 5:
                            return 'NULL';

                        case 6:
                            return 'OBJECT_IDENTIFIER';

                        case 7:
                            return 'ObjectDescriptor';

                        case 8:
                            return 'EXTERNAL';

                        case 9:
                            return 'REAL';

                        case 10:
                            return 'ENUMERATED';

                        case 11:
                            return 'EMBEDDED_PDV';

                        case 12:
                            return 'UTF8String';

                        case 16:
                            return 'SEQUENCE';

                        case 17:
                            return 'SET';

                        case 18:
                            return 'NumericString';

                        case 19:
                            return 'PrintableString';
                        // ASCII subset

                        case 20:
                            return 'TeletexString';
                        // aka T61String

                        case 21:
                            return 'VideotexString';

                        case 22:
                            return 'IA5String';
                        // ASCII

                        case 23:
                            return 'UTCTime';

                        case 24:
                            return 'GeneralizedTime';

                        case 25:
                            return 'GraphicString';

                        case 26:
                            return 'VisibleString';
                        // ASCII subset

                        case 27:
                            return 'GeneralString';

                        case 28:
                            return 'UniversalString';

                        case 30:
                            return 'BMPString';
                    }

                    return 'Universal_' + this.tag.tagNumber.toString();

                case 1:
                    return 'Application_' + this.tag.tagNumber.toString();

                case 2:
                    return '[' + this.tag.tagNumber.toString() + ']';
                // Context

                case 3:
                    return 'Private_' + this.tag.tagNumber.toString();
            }
        };

        ASN1.prototype.content = function (maxLength) {
            if (this.tag === undefined) {
                return null;
            }

            if (maxLength === undefined) {
                maxLength = Infinity;
            }

            var content = this.posContent();
            var len = Math.abs(this.length);

            if (!this.tag.isUniversal()) {
                if (this.sub !== null) {
                    return '(' + this.sub.length + ' elem)';
                }

                return this.stream.parseOctetString(content, content + len, maxLength);
            }

            switch (this.tag.tagNumber) {
                case 1:
                    // BOOLEAN
                    return this.stream.get(content) === 0 ? 'false' : 'true';

                case 2:
                    // INTEGER
                    return this.stream.parseInteger(content, content + len);

                case 3:
                    // BIT_STRING
                    return this.sub ? '(' + this.sub.length + ' elem)' : this.stream.parseBitString(content, content + len, maxLength);

                case 4:
                    // OCTET_STRING
                    return this.sub ? '(' + this.sub.length + ' elem)' : this.stream.parseOctetString(content, content + len, maxLength);
                // case 0x05: // NULL

                case 6:
                    // OBJECT_IDENTIFIER
                    return this.stream.parseOID(content, content + len, maxLength);
                // case 0x07: // ObjectDescriptor
                // case 0x08: // EXTERNAL
                // case 0x09: // REAL
                // case 0x0A: // ENUMERATED
                // case 0x0B: // EMBEDDED_PDV

                case 16: // SEQUENCE

                case 17:
                    // SET
                    if (this.sub !== null) {
                        return '(' + this.sub.length + ' elem)';
                    } else {
                        return '(no elem)';
                    }

                case 12:
                    // UTF8String
                    return stringCut(this.stream.parseStringUTF(content, content + len), maxLength);

                case 18: // NumericString

                case 19: // PrintableString

                case 20: // TeletexString

                case 21: // VideotexString

                case 22: // IA5String
                // case 0x19: // GraphicString

                case 26:
                    // VisibleString
                    // case 0x1B: // GeneralString
                    // case 0x1C: // UniversalString
                    return stringCut(this.stream.parseStringISO(content, content + len), maxLength);

                case 30:
                    // BMPString
                    return stringCut(this.stream.parseStringBMP(content, content + len), maxLength);

                case 23: // UTCTime

                case 24:
                    // GeneralizedTime
                    return this.stream.parseTime(content, content + len, this.tag.tagNumber == 23);
            }

            return null;
        };

        ASN1.prototype.toString = function () {
            return this.typeName() + '@' + this.stream.pos + '[header:' + this.header + ',length:' + this.length + ',sub:' + (this.sub === null ? 'null' : this.sub.length) + ']';
        };

        ASN1.prototype.toPrettyString = function (indent) {
            if (indent === undefined) {
                indent = '';
            }

            var s = indent + this.typeName() + ' @' + this.stream.pos;

            if (this.length >= 0) {
                s += '+';
            }

            s += this.length;

            if (this.tag.tagConstructed) {
                s += ' (constructed)';
            } else {
                if (this.tag.isUniversal() && (this.tag.tagNumber == 3 || this.tag.tagNumber == 4) && this.sub !== null) {
                    s += ' (encapsulates)';
                }
            }

            s += '\n';

            if (this.sub !== null) {
                indent += '  ';

                for (var i = 0, max = this.sub.length; i < max; ++i) {
                    s += this.sub[i].toPrettyString(indent);
                }
            }

            return s;
        };

        ASN1.prototype.posStart = function () {
            return this.stream.pos;
        };

        ASN1.prototype.posContent = function () {
            return this.stream.pos + this.header;
        };

        ASN1.prototype.posEnd = function () {
            return this.stream.pos + this.header + Math.abs(this.length);
        };

        ASN1.prototype.toHexString = function () {
            return this.stream.hexDump(this.posStart(), this.posEnd(), true);
        };

        ASN1.decodeLength = function (stream) {
            var buf = stream.get();
            var len = buf & 127;

            if (len == buf) {
                return len;
            } // no reason to use Int10, as it would be a huge buffer anyways

            if (len > 6) {
                throw new Error('Length over 48 bits not supported at position ' + (stream.pos - 1));
            }

            if (len === 0) {
                return null;
            } // undefined

            buf = 0;

            for (var i = 0; i < len; ++i) {
                buf = buf * 256 + stream.get();
            }

            return buf;
        };
        /**
         * Retrieve the hexadecimal value (as a string) of the current ASN.1 element
         * @returns {string}
         * @public
         */

        ASN1.prototype.getHexStringValue = function () {
            var hexString = this.toHexString();
            var offset = this.header * 2;
            var length = this.length * 2;
            return hexString.substr(offset, length);
        };

        ASN1.decode = function (str) {
            var stream;

            if (!(str instanceof Stream)) {
                stream = new Stream(str, 0);
            } else {
                stream = str;
            }

            var streamStart = new Stream(stream);
            var tag = new ASN1Tag(stream);
            var len = ASN1.decodeLength(stream);
            var start = stream.pos;
            var header = start - streamStart.pos;
            var sub = null;

            var getSub = function () {
                var ret = [];

                if (len !== null) {
                    // definite length
                    var end = start + len;

                    while (stream.pos < end) {
                        ret[ret.length] = ASN1.decode(stream);
                    }

                    if (stream.pos != end) {
                        throw new Error('Content size is not correct for container starting at offset ' + start);
                    }
                } else {
                    // undefined length
                    try {
                        for (;;) {
                            var s = ASN1.decode(stream);

                            if (s.tag.isEOC()) {
                                break;
                            }

                            ret[ret.length] = s;
                        }

                        len = start - stream.pos; // undefined lengths are represented as negative values
                    } catch (e) {
                        throw new Error('Exception while decoding undefined length content: ' + e);
                    }
                }

                return ret;
            };

            if (tag.tagConstructed) {
                // must have valid content
                sub = getSub();
            } else {
                if (tag.isUniversal() && (tag.tagNumber == 3 || tag.tagNumber == 4)) {
                    // sometimes BitString and OctetString are used to encapsulate ASN.1
                    try {
                        if (tag.tagNumber == 3) {
                            if (stream.get() != 0) {
                                throw new Error('BIT STRINGs with unused bits cannot encapsulate.');
                            }
                        }

                        sub = getSub();

                        for (var i = 0; i < sub.length; ++i) {
                            if (sub[i].tag.isEOC()) {
                                throw new Error('EOC is not supposed to be actual content.');
                            }
                        }
                    } catch (e) {
                        // but silently ignore when they don't
                        sub = null;
                    }
                }
            }

            if (sub === null) {
                if (len === null) {
                    throw new Error("We can't skip over an invalid tag with undefined length at offset " + start);
                }

                stream.pos = start + Math.abs(len);
            }

            return new ASN1(streamStart, header, len, tag, sub);
        };

        return ASN1;
    })();

export { ASN1 };

var ASN1Tag =
    /** @class */
    (function () {
        function ASN1Tag(stream) {
            var buf = stream.get();
            this.tagClass = buf >> 6;
            this.tagConstructed = (buf & 32) !== 0;
            this.tagNumber = buf & 31;

            if (this.tagNumber == 31) {
                // long tag
                var n = new Int10();

                do {
                    buf = stream.get();
                    n.mulAdd(128, buf & 127);
                } while (buf & 128);

                this.tagNumber = n.simplify();
            }
        }

        ASN1Tag.prototype.isUniversal = function () {
            return this.tagClass === 0;
        };

        ASN1Tag.prototype.isEOC = function () {
            return this.tagClass === 0 && this.tagNumber === 0;
        };

        return ASN1Tag;
    })();

export { ASN1Tag };
